﻿using System.Security.Cryptography;
using System.Text;
using ApiLib.AppStart;

namespace ApiLib.Utils;

/// <summary>
/// GoogleAuthenticator工具
/// <example>
/// <code>
/// //获取口令
/// var info = new GoogleAuthenticatorInfo(phone);
/// var generateCode = info.GoogleAuthenticator.GenerateCode();
/// return new{info.GoogleAuthenticator.ExpireSeconds, Code = generateCode};
/// 
/// //检验
/// if (!GoogleAuthenticatorUtil.Verify(phone, code)) throw ActionException("无效验证码！");
/// </code>
/// </example>
/// </summary>
public static class GoogleAuthenticatorUtil
{
    private static readonly List<GoogleAuthenticatorInfo> AuthenticatorInfos = new();

    /// <summary>
    /// 获取手机端秘钥
    /// </summary>
    /// <param name="phone"></param>
    /// <returns></returns>
    public static string GetPhoneKey(string phone)
    {
        var authenticatorInfo = GetGoogleAuthenticatorInfo(phone);
        return authenticatorInfo.GoogleAuthenticator.GetMobilePhoneKey();
    }

    /// <summary>
    /// 验证
    /// </summary>
    /// <param name="phone"></param>
    /// <param name="code"></param>
    /// <returns></returns>
    public static bool Verify(string phone, string code)
    {
        var authenticatorInfo = GetGoogleAuthenticatorInfo(phone);
        return authenticatorInfo.GoogleAuthenticator.GenerateCode() == code;
    }

    private static GoogleAuthenticatorInfo GetGoogleAuthenticatorInfo(string phone)
    {
        var time = DateTime.Now.ToString("MdH");
        var authenticatorInfo = AuthenticatorInfos.Find(info => info.MobilePhone == phone && info.Time == time);
        if (authenticatorInfo == null)
        {
            AuthenticatorInfos.RemoveAll(info => info.MobilePhone == phone);
            authenticatorInfo = new GoogleAuthenticatorInfo(phone);
            AuthenticatorInfos.Add(authenticatorInfo);
        }

        return authenticatorInfo;
    }
}

/// <summary>
/// 谷歌二次认证信息
/// </summary>
public class GoogleAuthenticatorInfo
{
    /// <summary>
    /// 手机号
    /// </summary>
    public string MobilePhone { get; set; }

    /// <summary>
    /// GoogleAuthenticator
    /// </summary>
    public GoogleAuthenticator GoogleAuthenticator { get; }

    /// <summary>
    /// 时间
    /// </summary>
    public string Time { get; }

    /// <summary>
    /// 构造方法
    /// </summary>
    /// <param name="phone"></param>
    /// <param name="key"></param>
    public GoogleAuthenticatorInfo(string phone,string? key=null)
    {
        if (string.IsNullOrEmpty(key))
        {
            key=GlobalConfig.AppConfig?.GoogleAuthenticatorKey ?? "Server";
        }
        MobilePhone = phone;
        Time = DateTime.Now.ToString("MdH");
        var num = phone.Length > 4 ? phone[^4..] : phone;
        GoogleAuthenticator = new GoogleAuthenticator(key + num + Time);
    }
}

/// <summary>
/// 谷歌二次认证
/// </summary>
public class GoogleAuthenticator
{
    /// <summary>
    /// 初始化验证码生成规则
    /// </summary>
    /// <param name="key">秘钥(手机使用Base32码)</param>
    public GoogleAuthenticator(string key)
    {
        SecretKey = key;
        SecretKeyMobile = Base32.ToString(Encoding.UTF8.GetBytes(key));
        DurationTime = 30;
    }

    /// <summary>
    /// 初始化验证码生成规则
    /// </summary>
    /// <param name="key">秘钥(手机使用Base32码)</param>
    /// <param name="duration">验证码间隔多久刷新一次（默认30秒和google同步）</param>
    public GoogleAuthenticator(long duration = 30, string key = "test")
    {
        SecretKey = key;
        SecretKeyMobile = Base32.ToString(Encoding.UTF8.GetBytes(key));
        DurationTime = duration;
    }

    /// <summary>
    /// 间隔时间
    /// </summary>
    private long DurationTime { get; set; }

    /// <summary>
    /// 迭代次数
    /// </summary>
    private long Counter =>
        (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds /
        DurationTime;

    /// <summary>
    /// 秘钥
    /// </summary>
    private string SecretKey { get; set; }

    /// <summary>
    /// 手机端输入的秘钥
    /// </summary>
    private string SecretKeyMobile { get; init; }

    /// <summary>
    /// 到期秒数
    /// </summary>
    public long ExpireSeconds =>
        (DurationTime -
         (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds %
         DurationTime);

    /// <summary>
    /// 获取手机端秘钥
    /// </summary>
    /// <returns></returns>
    public string GetMobilePhoneKey()
    {
        if (SecretKeyMobile == null)
            throw new ArgumentNullException(nameof(SecretKeyMobile));
        return SecretKeyMobile;
    }

    /// <summary>
    /// 生成认证码
    /// </summary>
    /// <returns>返回验证码</returns>
    public string GenerateCode()
    {
        return GenerateHashedCode(SecretKey, Counter);
    }

    /// <summary>
    /// 按照次数生成哈希编码
    /// </summary>
    /// <param name="secret">秘钥</param>
    /// <param name="iterationNumber">迭代次数</param>
    /// <param name="digits">生成位数</param>
    /// <returns>返回验证码</returns>
    private string GenerateHashedCode(string secret, long iterationNumber, int digits = 6)
    {
        var counter = BitConverter.GetBytes(iterationNumber);

        if (BitConverter.IsLittleEndian)
            Array.Reverse(counter);

        var key = Encoding.ASCII.GetBytes(secret);

        var hmac = new HMACSHA1(key, true);

        var hash = hmac.ComputeHash(counter);

        var offset = hash[hash.Length - 1] & 0xf;

        var binary =
            ((hash[offset] & 0x7f) << 24)
            | ((hash[offset + 1] & 0xff) << 16)
            | ((hash[offset + 2] & 0xff) << 8)
            | (hash[offset + 3] & 0xff);

        var password = binary % (int)Math.Pow(10, digits); // 6 digits

        return password.ToString(new string('0', digits));
    }
}

/// <summary>
/// 工具类
/// </summary>
public static class Base32
{
    /// <summary>
    /// string -> bytes
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    /// <exception cref="ArgumentNullException"></exception>
    public static byte[] ToBytes(string input)
    {
        if (string.IsNullOrEmpty(input))
        {
            throw new ArgumentNullException(nameof(input));
        }

        input = input.TrimEnd('=');
        var byteCount = input.Length * 5 / 8;
        var returnArray = new byte[byteCount];

        byte curByte = 0, bitsRemaining = 8;
        var arrayIndex = 0;

        foreach (var c in input)
        {
            var cValue = CharToValue(c);

            var mask = 0;
            if (bitsRemaining > 5)
            {
                mask = cValue << (bitsRemaining - 5);
                curByte = (byte)(curByte | mask);
                bitsRemaining -= 5;
            }
            else
            {
                mask = cValue >> (5 - bitsRemaining);
                curByte = (byte)(curByte | mask);
                returnArray[arrayIndex++] = curByte;
                curByte = (byte)(cValue << (3 + bitsRemaining));
                bitsRemaining += 3;
            }
        }

        if (arrayIndex != byteCount)
        {
            returnArray[arrayIndex] = curByte;
        }

        return returnArray;
    }

    /// <summary>
    /// byte[] -> string
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    /// <exception cref="ArgumentNullException"></exception>
    public static string ToString(byte[] input)
    {
        if (input == null || input.Length == 0)
        {
            throw new ArgumentNullException(nameof(input));
        }

        int charCount = (int)Math.Ceiling(input.Length / 5d) * 8;
        char[] returnArray = new char[charCount];

        byte nextChar = 0, bitsRemaining = 5;
        int arrayIndex = 0;

        foreach (byte b in input)
        {
            nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining)));
            returnArray[arrayIndex++] = ValueToChar(nextChar);

            if (bitsRemaining < 4)
            {
                nextChar = (byte)((b >> (3 - bitsRemaining)) & 31);
                returnArray[arrayIndex++] = ValueToChar(nextChar);
                bitsRemaining += 5;
            }

            bitsRemaining -= 3;
            nextChar = (byte)((b << bitsRemaining) & 31);
        }

        if (arrayIndex != charCount)
        {
            returnArray[arrayIndex++] = ValueToChar(nextChar);
            while (arrayIndex != charCount) returnArray[arrayIndex++] = '=';
        }

        return new string(returnArray);
    }

    private static int CharToValue(char c)
    {
        var value = (int)c;

        if (value is < 91 and > 64)
        {
            return value - 65;
        }

        if (value is < 56 and > 49)
        {
            return value - 24;
        }

        if (value is < 123 and > 96)
        {
            return value - 97;
        }

        throw new ArgumentException("Character is not a Base32 character.", nameof(c));
    }

    private static char ValueToChar(byte b)
    {
        if (b < 26)
        {
            return (char)(b + 65);
        }

        if (b < 32)
        {
            return (char)(b + 24);
        }

        throw new ArgumentException("Byte is not a value Base32 value.", nameof(b));
    }
}